package gsn.http.rest;

import gsn.DataDistributer;
import gsn.Main;
import gsn.Mappings;
import gsn.ModelDistributer;
import gsn.VirtualSensor;
import gsn.beans.VSensorConfig;
import gsn.http.WebConstants;
import gsn.http.ac.DataSource;
import gsn.http.ac.GeneralServicesAPI;
import gsn.http.ac.User;
import gsn.storage.SQLUtils;
import gsn.storage.SQLValidator;
import gsn.utils.Helpers;
import gsn.utils.models.AbstractModel;
import gsn.vsensor.AbstractVirtualSensor;
import gsn.vsensor.ModellingVirtualSensor;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.StringTokenizer;
import java.util.concurrent.LinkedBlockingQueue;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.http.HttpStatus;
import org.apache.log4j.Logger;
import org.eclipse.jetty.continuation.Continuation;
import org.eclipse.jetty.continuation.ContinuationSupport;

public class RestStreamHanlder extends HttpServlet {

	public static final int SUCCESS_200 = 200;

	private static final int _300 = 300;

	private static final String STREAMING = "/streaming/";
	private static final String PARAMETER_REG_ID = "regId";
	
	private static transient Logger logger = Logger
			.getLogger(RestStreamHanlder.class);

	public void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException {

		logger.warn("RestStreamHanlder: doGet");
		
		Object is2ndPass = request.getAttribute("2ndPass");
		Continuation continuation = ContinuationSupport.getContinuation(request);

		if (continuation.isExpired()) {
			logger.debug("Continuation has expired.");
			return;
		}

		String[] cred = null;
		if (Main.getContainerConfig().isAcEnabled()) {
			cred = parseAuthorizationHeader(request);
			if (cred == null) {
				try {
					response.setHeader("WWW-Authenticate",
							"Basic realm=\"GSN Access Control\"");// set the supported
																										// challenge
					response
							.sendError(HttpStatus.SC_UNAUTHORIZED, "Unauthorized access.");
				}
				catch (IOException e) {
					logger.debug(e.getMessage(), e);
				}
				return;
			}
		}

		if (is2ndPass == null) {
			continuation.setAttribute("2ndPass", Boolean.TRUE);
			continuation.setAttribute("status", new LinkedBlockingQueue<Boolean>(1));
			continuation.setTimeout(-1); // Disable the timeout on the continuation.
			continuation.suspend();
			final DefaultDistributionRequest streamingReq;
			try {
				URLParser parser = new URLParser(request);
				//
				if (Main.getContainerConfig().isAcEnabled()) {
					String vsName = parser.getVSensorConfig().getName();
					if (DataSource.isVSManaged(vsName)) {
						User user = GeneralServicesAPI.getInstance().doLogin(cred[0],
								cred[1]);
						if ((user == null || (!user.isAdmin() && !user
								.hasReadAccessRight(vsName)))) {
							response.setHeader("WWW-Authenticate",
									"Basic realm=\"GSN Access Control\"");// set the supported
																												// challenge
							response.sendError(HttpStatus.SC_UNAUTHORIZED,
									"Unauthorized access.");
							return;
						}
					}
				}
				//
				RestDelivery deliverySystem = new RestDelivery(continuation);
				streamingReq = DefaultDistributionRequest
						.create(deliverySystem, parser.getVSensorConfig(),
								parser.getQuery(), parser.getStartTime());
				DataDistributer.getInstance(deliverySystem.getClass()).addListener(
						streamingReq);
			}
			catch (Exception e) {
				logger.warn(e.getMessage());
				continuation.complete();
			}
		}
		else {
			boolean status = false;
			try {
				status = !continuation.getServletResponse().getWriter().checkError();
			}
			catch (Exception e) {
				logger.debug(e.getMessage(), e);
			}
			continuation.suspend();
			try {
				((LinkedBlockingQueue<Boolean>) continuation.getAttribute("status"))
						.put(status);
			}
			catch (InterruptedException e) {
				logger.debug(e.getMessage(), e);
			}
		}
	}

	/**
	 * This happens at the server
	 */
	public void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException {
		logger.warn("RestStreamHanlder: doPost");
		
		try {
			URLParser parser = new URLParser(request);
			String vsName = parser.getVSensorConfig().getName();

			//
			if (Main.getContainerConfig().isAcEnabled()) {
				String[] cred = parseAuthorizationHeader(request);
				if (cred == null) {
					try {
						response.setHeader("WWW-Authenticate",
								"Basic realm=\"GSN Access Control\"");// set the supported
																											// challenge
						response.sendError(HttpStatus.SC_UNAUTHORIZED,
								"Unauthorized access.");
					}
					catch (IOException e) {
						logger.debug(e.getMessage(), e);
					}
					return;
				}
				if (DataSource.isVSManaged(vsName)) {
					User user = GeneralServicesAPI.getInstance()
							.doLogin(cred[0], cred[1]);
					if ((user == null || (!user.isAdmin() && !user
							.hasReadAccessRight(vsName)))) {
						response.setHeader("WWW-Authenticate",
								"Basic realm=\"GSN Access Control\"");// set the supported
																											// challenge
						response.sendError(HttpStatus.SC_UNAUTHORIZED,
								"Unauthorized access.");
						return;
					}
				}
			}
			//
			Double notificationId = Double.parseDouble(request
					.getParameter(PushDelivery.NOTIFICATION_ID_KEY));
			String localContactPoint = request
					.getParameter(PushDelivery.LOCAL_CONTACT_POINT);
			if (localContactPoint == null) {
				logger.warn("Push streaming request received without "
						+ PushDelivery.LOCAL_CONTACT_POINT + " parameter !");
				return;
			}
			// checking to see if there is an already registered notification id, in
			// that case, we ignore (re)registration.

			DeliverySystem delivery;
			if (parser.pushType.equals("wp"))
				delivery = new WPPushDelivery(localContactPoint, notificationId,
						response.getWriter(), parser.nClass, parser.nMessage);
			else if (parser.pushType.equals("ad")){
				String regId = request
						.getParameter(PARAMETER_REG_ID);
				
				delivery = new AndroidPushDelivery(localContactPoint, notificationId,
						response.getWriter(), parser.nClass, regId);
			}
			else
				delivery = new PushDelivery(localContactPoint, notificationId,
						response.getWriter());

			boolean isExist = DataDistributer.getInstance(delivery.getClass())
					.contains(delivery);
			if (isExist) {
				logger.debug("Keep alive request received for the notification-id:"
						+ notificationId);
				response.setStatus(SUCCESS_200);
				delivery.close();
				return;
			}
			if (parser.getModelClass() == null) { // query on the database
				DefaultDistributionRequest distributionReq = DefaultDistributionRequest
						.create(delivery, parser.getVSensorConfig(), parser.getQuery(),
								parser.getStartTime());
				logger.debug("Rest request received: " + distributionReq.toString());
				DataDistributer.getInstance(delivery.getClass()).addListener(
						distributionReq);
				logger.debug("Streaming request received and registered:"
						+ distributionReq.toString());
			}
			else { // query on a model
				ModelDistributionRequest distributionReq = ModelDistributionRequest
						.create(delivery, parser.getVSensorConfig(), parser.getQuery(),
								parser.getModelClass());
				logger.warn("Rest request received: " + distributionReq.toString());
				ModelDistributer.getInstance(delivery.getClass()).addListener(
						distributionReq);
				logger.warn("Streaming request received and registered:"
						+ distributionReq.toString());
			}

		}
		catch (Exception e) {
			logger.warn(e.getMessage(), e);
			return;
		}
	}

	/**
	 * This happens at the client
	 */
	public void doPut(HttpServletRequest request, HttpServletResponse response)
			throws ServletException {
		double notificationId = Double.parseDouble(request
				.getParameter(PushDelivery.NOTIFICATION_ID_KEY));
		PushRemoteWrapper notification = NotificationRegistry.getInstance()
				.getNotification(notificationId);
		try {
			if (notification != null) {
				boolean status = notification.manualDataInsertion(request
						.getParameter(PushDelivery.DATA));
				if (status)
					response.setStatus(SUCCESS_200);
				else
					response.setStatus(_300);
			}
			else {
				logger
						.warn("Received a Http put request for an INVALID notificationId: "
								+ notificationId);
				response.sendError(_300);
			}
		}
		catch (IOException e) {
			logger.warn("Failed in writing the status code into the connection.\n"
					+ e.getMessage(), e);
		}
	}

	/**
	 * 
	 * @param request
	 * @return [username,password] or null if unable to retrieve these pieces of
	 *         information.
	 */
	private String[] parseAuthorizationHeader(HttpServletRequest request) {
		// Get username/password from the Authorization header
		String authHeader = request.getHeader("Authorization"); // form: BASIC
																														// d2VibWFzdGVyOnRyeTJndWVTUw
		if (authHeader != null) {
			String[] ahs = authHeader.split(" ");
			if (ahs.length == 2) {
				String b64UsernamPassword = ahs[1]; // we get:
																						// d2VibWFzdGVyOnRyeTJndWVTUw
				sun.misc.BASE64Decoder decoder = new sun.misc.BASE64Decoder();
				try {
					String userPass = new String(decoder.decodeBuffer(b64UsernamPassword)); // form:
																																									// username:passsword
					String[] ups;
					if ((ups = userPass.split(":")).length == 2) {
						return new String[] { ups[0], // username
								ups[1] // password
						};
					}
				}
				catch (IOException e) {
					logger.debug(e.getMessage(), e);
				}
			}
		}
		return null;
	}

	class URLParser {
		private String query, tableName;
		private String cName = null;
		private AbstractModel modelClass = null;
		private String pushType = "";
		private int nClass = 0;
		private long startTime;
		private String nMessage = "";
		private VSensorConfig config;

		public URLParser(HttpServletRequest request)
				throws UnsupportedEncodingException, Exception {
			
			String requestURI = request.getRequestURI().substring(
					request.getRequestURI().toLowerCase().indexOf(STREAMING)
							+ STREAMING.length());
			
			StringTokenizer tokens = new StringTokenizer(requestURI, "/");
			startTime = System.currentTimeMillis();
			String first = tokens.nextToken();
			if (first.startsWith("wp")) { // registering from a Windows Phone
				pushType = "wp";
				nClass = Integer.parseInt(first.substring(2, 3));
				nMessage = URLDecoder.decode(first.substring(3), "UTF-8");
				query = tokens.nextToken();
			}
			else if (first.startsWith("ad")) { // registering from a Android Phone
				pushType = "ad";
				query = tokens.nextToken();
			}
			else {
				query = first;
			}
			query = URLDecoder.decode(query, "UTF-8");
			while (tokens.hasMoreTokens()) {
				String nt = tokens.nextToken();
				if (nt.startsWith("using")) {
					cName = nt.substring(6);
				}
				else {
					startTime = Helpers.convertTimeFromIsoToLong(URLDecoder.decode(nt,
							"UTF-8"));
				}
			}
			tableName = SQLValidator.getInstance().validateQuery(query);
			if (tableName == null)
				throw new RuntimeException("Bad Table name in the query:" + query);
			/**
			 * IMPORTANT: We change the table names to lower-case as some databases
			 * (e.g., MySQL on linux) are case sensitive and in general we use lower
			 * case for table names in GSN.
			 **/
			tableName = tableName.trim();
			query = SQLUtils.newRewrite(query, tableName, tableName.toLowerCase())
					.toString();
			tableName = tableName.toLowerCase();
			config = Mappings.getConfig(tableName);
			int modelNB = 0;
			try {
				modelNB = Integer.parseInt(cName);
			}
			catch (Exception e) {
			}
			VirtualSensor vs = Mappings.getVSensorInstanceByFileName(config
					.getFileName());
			AbstractVirtualSensor avs = vs.borrowVS();
			if (avs instanceof ModellingVirtualSensor) {
				modelClass = ((ModellingVirtualSensor) avs).getModel(modelNB);
			}
			vs.returnVS(avs);

		}

		public VSensorConfig getVSensorConfig() {
			return config;
		}

		public String getQuery() {
			return query;
		}

		public long getStartTime() {
			return startTime;
		}

		public AbstractModel getModelClass() {
			return modelClass;
		}

	}

}
